package com.damocode.starter.httpclient.helper;

import com.damocode.starter.httpclient.autoconfigure.HttpClientPoolProperties;
import com.damocode.starter.httpclient.domain.HttpRequest;
import com.damocode.starter.httpclient.domain.HttpResponse;
import com.damocode.starter.httpclient.exceptions.ClientException;
import com.damocode.starter.httpclient.utils.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.Closeable;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class HttpClientHelper implements Closeable {

    private static volatile HttpClientHelper httpClientHelper = null;

    private AtomicBoolean initialized = new AtomicBoolean(false);
    private CountDownLatch latch = new CountDownLatch(1);
    private PoolingHttpClientConnectionManager connectionManager;
    private ExecutorService executorService;
    private CloseableHttpClient httpClient;

    private HttpClientPoolProperties httpClientPoolProperties;

    public static HttpClientHelper getInstance(HttpClientPoolProperties httpClientPoolProperties) throws ClientException {
        if (httpClientHelper == null) {
            synchronized (HttpClientHelper.class) {
                if (httpClientHelper == null) {
                    httpClientHelper = new HttpClientHelper(httpClientPoolProperties);
                }
            }
        }
        return httpClientHelper;
    }

    private HttpClientHelper(HttpClientPoolProperties httpClientPoolProperties) throws ClientException {
        this.httpClientPoolProperties = httpClientPoolProperties;
        this.init(httpClientPoolProperties);
    }

    private void initConnectionManager() {
        RegistryBuilder<ConnectionSocketFactory> socketFactoryRegistryBuilder = RegistryBuilder.create();
        socketFactoryRegistryBuilder.register("http", PlainConnectionSocketFactory.INSTANCE);
        SSLConnectionSocketFactory sslConnectionSocketFactory = this.createSSLConnectionSocketFactory();
        socketFactoryRegistryBuilder.register("https", sslConnectionSocketFactory);
        this.connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistryBuilder.build());
        this.connectionManager.setMaxTotal(this.httpClientPoolProperties.getMaxTotal());
        this.connectionManager.setDefaultMaxPerRoute(this.httpClientPoolProperties.getDefaultMaxPerRoute());
    }

    private static SSLConnectionSocketFactory createSSLConnectionSocketFactory() {
        try {
            SSLContext sslcontext = SSLContexts.custom()
                    .loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true)
                    .build();
            return new SSLConnectionSocketFactory(sslcontext,
                    SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            e.printStackTrace();
        }
        return null;
    }

    protected void init(HttpClientPoolProperties httpClientPoolProperties) throws ClientException {
        if (!this.initialized.compareAndSet(false, true)) {
            try {
                this.latch.await();
            } catch (InterruptedException e) {
                throw new ClientException("SDK.InitFailed", "Init HttpClientHelper failed", e);
            }
        } else {
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectTimeout(httpClientPoolProperties.getConnectionTimeoutMillis())
                    .setSocketTimeout(httpClientPoolProperties.getSocketTimeout())
                    .setConnectionRequestTimeout(httpClientPoolProperties.getConnectionRequestTimeout())
                    .build();
            this.initConnectionManager();
            HttpClientBuilder builder = HttpClientBuilder.create()
                    .setDefaultRequestConfig(requestConfig)
                    .setConnectionManager(this.connectionManager);
            ApacheIdleConnectionCleaner.registerConnectionManager(this.connectionManager, httpClientPoolProperties.getMaxIdleTimeMillis());
            this.executorService = new ThreadPoolExecutor(0, this.httpClientPoolProperties.getMaxTotal(), 60L, TimeUnit.SECONDS, new SynchronousQueue(), new DefaultAsyncThreadFactory());
            if (httpClientPoolProperties.getKeepAliveDurationMillis() > 0L) {
                builder.setKeepAliveStrategy((response, context) -> {
                    long duration = DefaultConnectionKeepAliveStrategy.INSTANCE.getKeepAliveDuration(response, context);
                    return duration > 0L && duration < httpClientPoolProperties.getKeepAliveDurationMillis() ? duration : httpClientPoolProperties.getKeepAliveDurationMillis();
                });
            }
            this.httpClient = builder.build();
            this.latch.countDown();
        }
    }

    public final HttpResponse syncInvoke(HttpRequest apiRequest) throws IOException, ClientException{
        HttpUriRequest httpRequest = apiRequest.parseToHttpRequest(this.httpClientPoolProperties);
        CloseableHttpResponse httpResponse = null;
        HttpResponse result;
        try {
            httpResponse = this.httpClient.execute(httpRequest);
            result = parseToHttpResponse(httpResponse,apiRequest.getEncoding());
        }finally {
            IOUtils.closeQuietly(httpResponse);
        }
        return result;
    }

    public final Future<HttpResponse> asyncInvoke(final HttpRequest apiRequest, final CallBack callback){
        return this.executorService.submit(() -> {
            HttpResponse result;
            try {
                result = HttpClientHelper.this.syncInvoke(apiRequest);
            } catch (Exception e) {
                if (callback != null) {
                    callback.onFailure(apiRequest, e);
                }
                throw e;
            }
            if (callback != null) {
                callback.onResponse(apiRequest, result);
            }
            return result;
        });
    }

    private HttpResponse parseToHttpResponse(CloseableHttpResponse httpResponse, String defaultCharset) throws IOException {
        HttpResponse result = new HttpResponse();
        result.setStatusCode(httpResponse.getStatusLine().getStatusCode());
        result.setReasonPhrase(httpResponse.getStatusLine().getReasonPhrase());
        HttpEntity httpEntity = httpResponse.getEntity();
        if (httpEntity != null && (httpEntity.getContentLength() > 0L || httpEntity.isChunked())) {
            Header contentTypeHeader = httpEntity.getContentType();
            ContentType contentType = ContentType.parse(contentTypeHeader.getValue());
            FormatType formatType = FormatType.mapAcceptToFormat(contentType.getMimeType());
            result.setContentType(formatType);
            String charset =  contentType.getCharset() != null ? contentType.getCharset().toString() : defaultCharset;
            result.setContent(EntityUtils.toString(httpResponse.getEntity(),charset),charset, formatType);
        }

        Header[] headers = httpResponse.getAllHeaders();
        for(int i = 0; i <  headers.length; ++i) {
            Header header = headers[i];
            result.putHeaderParameter(header.getName(), header.getValue());
        }

        return result;
    }

    public void close() throws IOException {
        httpClientHelper = null;
        if (this.initialized.compareAndSet(true, false)) {
            this.executorService.shutdown();
            ApacheIdleConnectionCleaner.removeConnectionManager(this.connectionManager);
            this.connectionManager.shutdown();
            IOUtils.closeQuietly(this.httpClient);
        }
    }

    private class DefaultAsyncThreadFactory implements ThreadFactory {
        private AtomicInteger counter;

        private DefaultAsyncThreadFactory() {
            this.counter = new AtomicInteger(0);
        }

        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, "HttpClient_SDK_Async_ThreadPool_" + this.counter.incrementAndGet());
        }
    }

}
